package io.kiki.stack.http.feign.stream;

import io.kiki.stack.http.feign.FeignException;
import io.kiki.stack.http.feign.Response;
import io.kiki.stack.http.feign.codec.Decoder;

import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.Optional;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static io.kiki.stack.http.feign.Util.ensureClosed;

/**
 * Iterator based decoder that support streaming.
 *
 * <p>
 *
 * <p>Example: <br>
 *
 * <pre>
 * <code>
 * Feign.builder()
 *   .decoder(StreamDecoder.create(JacksonIteratorDecoder.create()))
 *   .doNotCloseAfterDecode() // Required for streaming
 *   .target(GitHub.class, "https://api.github.com");
 * or
 * Feign.builder()
 *   .decoder(StreamDecoder.create(JacksonIteratorDecoder.create(), (r, t) -> "hello world")))
 *   .doNotCloseAfterDecode() // Required for streaming
 *   .target(GitHub.class, "https://api.github.com");
 * interface GitHub {
 *  {@literal @}RequestLine("GET /repos/{owner}/{repo}/contributors")
 *   Stream<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
 * }</code>
 * </pre>
 */
public final class StreamDecoder implements Decoder {

    private final Decoder iteratorDecoder;
    private final Optional<Decoder> delegateDecoder;

    StreamDecoder(Decoder iteratorDecoder, Decoder delegateDecoder) {
        this.iteratorDecoder = iteratorDecoder;
        this.delegateDecoder = Optional.ofNullable(delegateDecoder);
    }

    public static boolean isStream(Type type) {
        if (!(type instanceof ParameterizedType parameterizedType)) {
            return false;
        }
        return parameterizedType.getRawType().equals(Stream.class);
    }

    public static StreamDecoder create(Decoder iteratorDecoder) {
        return new StreamDecoder(iteratorDecoder, null);
    }

    public static StreamDecoder create(Decoder iteratorDecoder, Decoder delegateDecoder) {
        return new StreamDecoder(iteratorDecoder, delegateDecoder);
    }

    @Override
    public Object decode(Response response, Type type) throws IOException, FeignException {
        if (!isStream(type)) {
            if (!delegateDecoder.isPresent()) {
                throw new IllegalArgumentException("StreamDecoder supports types other than stream. " + "When type is not stream, the delegate decoder needs to be setting.");
            } else {
                return delegateDecoder.get().decode(response, type);
            }
        }
        ParameterizedType parameterizedType = (ParameterizedType) type;
        Iterator<?> iterator = (Iterator<?>) iteratorDecoder.decode(response, new IteratorParameterizedType(parameterizedType));

        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false).onClose(() -> {
            if (iterator instanceof Closeable) {
                ensureClosed((Closeable) iterator);
            } else {
                ensureClosed(response);
            }
        });
    }

    static final class IteratorParameterizedType implements ParameterizedType {

        private final ParameterizedType streamType;

        IteratorParameterizedType(ParameterizedType streamType) {
            this.streamType = streamType;
        }

        @Override
        public Type[] getActualTypeArguments() {
            return streamType.getActualTypeArguments();
        }

        @Override
        public Type getRawType() {
            return Iterator.class;
        }

        @Override
        public Type getOwnerType() {
            return null;
        }
    }
}
